Asset Classes: Stocks


Kevin Crotty
BUSI 448: Investments

Asset Classes

  • Equity markets

  • Fixed income markets

  • Money markets

Today, we’ll focus on stock markets.

Stock Market Indices

  • SPY
  • Dow Jones
  • Russell
  • FTSE

Annual Returns

Annual SPY returns

SPY = S&P 500 Exchange Traded Fund (ETF)


Code
from pandas_datareader import DataReader as pdr
import plotly.graph_objects as go
import numpy as np
spy = pdr('SPY', 'yahoo', start=1990)['Adj Close']
spy = spy.resample('Y').last()
spy = spy.pct_change().dropna()
spy.index = spy.index.to_period('Y').astype(str)
spy = spy.reset_index()
spy.columns = ['date', 'ret']
trace = go.Scatter(
    x=spy.date,
    y=spy.ret,
        hovertemplate="%{x}<br>%{y:.1%}<extra></extra>",
    name="",
    )
fig = go.Figure(trace)
fig.update_layout(
    template="plotly_dark",
    xaxis_title="",
    yaxis_title="",
    yaxis_tickformat=".0%",
    width=1000,
    height=460,
  )
fig.update_xaxes(title_font_size=24)
fig.update_yaxes(title_font_size=24)
fig.update_layout(font_size=20)
fig.show()

Compounded SPY returns

Code
compound = (1+spy.ret).cumprod()
trace = go.Scatter(
    x=spy.date,
    y=compound,
        hovertemplate="%{x}<br>$%{y:.2f}<extra></extra>",
    name="",
    )
fig = go.Figure(trace)
fig.update_layout(
    template="plotly_dark",
    xaxis_title="",
    yaxis_title="Accumulation from $1",
    yaxis_tickformat=".0f",
    width=1000,
    height=460,
    legend=dict(
        yanchor="top", 
        y=0.99, 
        xanchor="right", 
        x=0.99
    )
  )
fig.update_xaxes(title_font_size=24)
fig.update_yaxes(title_font_size=24)
fig.update_layout(font_size=20)
fig.show()


value of $1 investment with dividends reinvested

Compounded returns on log scale: motivation

  • Let’s look at accumulations from two hypothetical stocks.
    • stock 1: 10% per year
    • stock 1: 2% per year until 2000 and 10% afterwards
  • It will appear that stock 2 did nothing before 2000 and earned a lot less than stock 1 even after 2000.

Plot of the Example

Code
r1 = np.cumprod([1] + 51*[1.1])
r2 = np.cumprod([1] + 30*[1.02] + 21*[1.1])
years = np.arange(1970, 2022)
trace1 = go.Scatter(
  x=years,
  y=r1,
  mode="lines",
  name="stock 1"
)
trace2 = go.Scatter(
  x=years,
  y=r2,
  mode="lines",
  name="stock 2"
)
fig = go.Figure()
fig.add_trace(trace1)
fig.add_trace(trace2)
string = "year %{x}<br>accum = %{y:.2f}"
fig.update_traces(hovertemplate=string)
fig.update_layout(
    template="plotly_dark",
    xaxis_title="",
    yaxis_title="Accumulation",
    xaxis_title_font_size=24,
    yaxis_title_font_size=24,
    font_size=20,
    yaxis_tickprefix="$", 
    yaxis_tickformat=",.0f",
    width=1000,
    height=520,
    legend=dict(
        yanchor="top", 
        y=0.99, 
        xanchor="left", 
        x=0.1
    )
)
fig.show()

Log (base 10) of accumulation

Code
trace1 = go.Scatter(
  x=years,
  y=np.log10(r1),
  mode="lines",
  name="stock 1"
)
trace2 = go.Scatter(
  x=years,
  y=np.log10(r2),
  mode="lines",
  name="stock 2"
)
fig = go.Figure()
fig.add_trace(trace1)
fig.add_trace(trace2)
string = "year %{x}<br>log = %{y:.2f}"
fig.update_traces(hovertemplate=string)
fig.update_layout(
    template="plotly_dark",
    xaxis_title="",
    yaxis_title="Log of Accumulation",
    xaxis_title_font_size=24,
    yaxis_title_font_size=24,
    font_size=20,
    yaxis_tickformat=",.1f",
    width=1000,
    height=520,
    legend=dict(
        yanchor="top", 
        y=0.99, 
        xanchor="left", 
        x=0.1
    )
)
fig.show()

Map \(y\) tick labels to dollars

Code
trace1 = go.Scatter(
  x=years,
  y=r1,
  mode="lines",
  name="stock 1"
)
trace2 = go.Scatter(
  x=years,
  y=r2,
  mode="lines",
  name="stock 2"
)
fig = go.Figure()
fig.add_trace(trace1)
fig.add_trace(trace2)
string = "year %{x}<br>$%{y:.2f}"
fig.update_traces(hovertemplate=string)
fig.update_layout(
    template="plotly_dark",
    xaxis_title="",
    yaxis_title="Accumulation",
    xaxis_title_font_size=24,
    yaxis_title_font_size=24,
    font_size=20,
    yaxis_type="log",
    yaxis_tickformat=",.0f",
    yaxis_tickprefix="$",
    width=1000,
    height=520,
    legend=dict(
        yanchor="top", 
        y=0.99, 
        xanchor="left", 
        x=0.1
    ),
    yaxis = dict(
        tickmode = 'array',
        tickvals = [1, 2, 5, 10, 20, 50, 100],
    )
)
fig.show()

Compounded SPY returns on log scale

Code
trace = go.Scatter(
    x=spy.date,
    y=compound,
        hovertemplate="%{x}<br>$%{y:.2f}<extra></extra>",
    name="",
    )
fig = go.Figure(trace)
fig.update_layout(
    template="plotly_dark",
    xaxis_title="",
    yaxis_title="Accumulation from $1",
    xaxis_title_font_size=24,
    yaxis_title_font_size=24,
    font_size=20,
    yaxis_tickformat=".0f",
    yaxis_type="log",
    width=1000,
    height=460,
    yaxis = dict(
        tickmode = 'array',
        tickvals = [2, 5, 10, 16],
    )
  )
fig.show()


value of $1 investment with dividends reinvested

Box Plot

  • Box contains 25th percentile through 75th percentile.

  • Median is indicated as a line in the box.

  • Fences extend 1.5 times inter-quartile range from 25th and 75th percentiles or to the most extreme observation if that is closer to the box.

    • inter-quartile range = 75th minus 25th percentile
  • Points outside the fences are outliers.

    • If you simulate data from a normal distribution, there will typically be very few points outside the fences.

Box and density plots of annual SPY returns

Code
from scipy.stats import norm
from scipy.stats import gaussian_kde as kde
density = kde(spy.ret)
grid = np.linspace(np.min(spy.ret), np.max(spy.ret), 100)
trace1 = go.Scatter(
  x=grid,
  y=density(grid),
  mode="lines",
  name="actual"
)
trace2 = go.Scatter(
  x=grid, 
  y=norm.pdf(grid, np.mean(spy.ret), scale=np.std(spy.ret)), 
  mode="lines", 
  name="normal"
)
fig = go.Figure()
fig.add_trace(trace1)
fig.add_trace(trace2)
fig.update_layout(
    template="plotly_dark",
    xaxis_title="Annual Return",
    yaxis_title="",
    xaxis_title_font_size=24,
    yaxis_title_font_size=24,
    font_size=20,
    xaxis_tickformat=".0%",
    yaxis_tickformat="",
    width=1000,
    height=360,
     legend=dict(
        yanchor="top", 
        y=0.99, 
        xanchor="left", 
        x=0.01
    ),
)
fig.show()

Normal distribution has same mean and std dev as actual.
x-axis range is minimum to maximum return.

Code
trace = go.Box(
    x=spy.ret,
    text=spy.date,
    hovertemplate="%{text}<extra></extra>",
    name="",
    )
fig = go.Figure(trace)
fig.update_layout(
    template="plotly_dark",
    yaxis_title="",
    xaxis_title="Annual Return",
    xaxis_title_font_size=24,
    yaxis_title_font_size=24,
    font_size=20,
    xaxis_tickformat=".0%",
    yaxis_tickformat="",
    width=1000,
    height=500,
)
fig.show()

Autocorrelations

  • Autocorrelation is the correlation of a time series with its own lagged values.
  • Autocorrelation at lag 1 tells us whether the current value predicts the next one.
  • For monthly data, autocorrelation might be high at lag 12 (seasonality).

Autocorrelations of annual SPY returns

. .

Code
from statsmodels.graphics.tsaplots import plot_acf
import matplotlib.pyplot as plt
plt.style.use('classic')
plt.rcParams.update({'font.size': 26})
fig = plot_acf(spy.ret)
plt.xlabel("Year")
plt.ylabel("Autocorrelation")
plt.title("")
_ = fig.set_size_inches(18,9)

Does last year’s return predict this year’s?

Code
import statsmodels.formula.api as smf
spy['lag'] = spy.ret.shift()
spy = spy.dropna()
trace = go.Scatter(
  x=spy.lag,
  y=spy.ret,
  text=spy.date,
  mode="markers",
  hovertemplate="%{text}<extra></extra>",
  showlegend=False,
  marker=dict(size=15)
)
fig = go.Figure(trace)
result = smf.ols("ret ~ lag", data=spy).fit()
predict = result.params['Intercept'] + result.params['lag']*spy.lag
trace = go.Scatter(
  x=spy.lag,
  y=predict,
  mode="lines",
  name="regression line",
)
fig.add_trace(trace)
fig.update_layout(
    template="plotly_dark",
    xaxis_title="Lagged Return",
    yaxis_title="Return",
    xaxis_title_font_size=24,
    yaxis_title_font_size=24,
    font_size=20,
    xaxis_tickformat=".0%",
    yaxis_tickformat=".0%",
    width=1000,
    height=520,
)
fig.show()

No, the autocorrelation is almost zero.

Daily Returns

Daily SPY returns

Code
from pandas_datareader import DataReader as pdr
import plotly.graph_objects as go
import numpy as np
spy = pdr('SPY', 'yahoo', start=1990)['Adj Close']
spy = spy.pct_change().dropna()
spy.index = spy.index.astype(str)
spy = spy.reset_index()
spy.columns = ['date', 'ret']
trace = go.Scatter(
    x=spy.date,
    y=spy.ret,
        hovertemplate="%{x}<br>%{y:.1%}<extra></extra>",
    name="",
    )
fig = go.Figure(trace)
fig.update_layout(
    template="plotly_dark",
    xaxis_title="",
    yaxis_title="",
    yaxis_title_font_size=24,
    xaxis_title_font_size=24,
    font_size=20,
    yaxis_tickformat=".0%",
    width=1000,
    height=500,
  )
fig.show()

Box and density plots of daily SPY returns

Code
from scipy.stats import norm
from scipy.stats import gaussian_kde as kde
density = kde(spy.ret)
grid = np.linspace(np.min(spy.ret), np.max(spy.ret), 100)
trace1 = go.Scatter(
  x=grid,
  y=density(grid),
  mode="lines",
  name="actual"
)
trace2 = go.Scatter(
  x=grid, 
  y=norm.pdf(grid, loc=np.mean(spy.ret), scale=np.std(spy.ret)), 
  mode="lines", 
  name="normal"
)
fig = go.Figure()
fig.add_trace(trace1)
fig.add_trace(trace2)
fig.update_layout(
    template="plotly_dark",
    xaxis_title="Daily Return",
    yaxis_title="",
    yaxis_title_font_size=24,
    xaxis_title_font_size=24,
    font_size=20,
    xaxis_tickformat=".0%",
    yaxis_tickformat="",
    width=1000,
    height=360,
     legend=dict(
        yanchor="top", 
        y=0.99, 
        xanchor="right", 
        x=0.99
    ),
)
fig.show()

Normal distribution has same mean and std dev as actual.
x-axis range is minimum to maximum return.

Code
trace = go.Box(
    x=spy.ret,
    text=spy.date,
    hovertemplate="%{text}<extra></extra>",
    name="",
    )
fig = go.Figure(trace)
fig.update_layout(
    template="plotly_dark",
    xaxis_title="Daily Return",
    yaxis_title="",
    yaxis_title_font_size=24,
    xaxis_title_font_size=24,
    font_size=20,
    xaxis_tickformat=".0%",
    yaxis_tickformat="",
    width=980,
    height=440,
)
fig.show()

Autocorrelations of daily SPY returns

Code
from statsmodels.graphics.tsaplots import plot_acf
import matplotlib.pyplot as plt
plt.style.use('classic')
plt.rcParams.update({'font.size': 26})
fig = plot_acf(spy.ret)
fig.set_size_inches(18,9)
plt.xlabel("Year")
plt.ylabel("Autocorrelation")
_ = plt.title("")

Does today’s return predict tomorrow’s?

Code
import statsmodels.formula.api as smf
spy['lag'] = spy.ret.shift()
spy = spy.dropna()
trace = go.Scatter(
  x=spy.lag,
  y=spy.ret,
  text=spy.date,
  mode="markers",
  hovertemplate="%{text}<extra></extra>",
  showlegend=False,
)
fig = go.Figure(trace)
result = smf.ols("ret ~ lag", data=spy).fit()
predict = result.params['Intercept'] + result.params['lag']*spy.lag
trace = go.Scatter(
  x=spy.lag,
  y=predict,
  mode="lines",
  name="regression line"
)
fig.add_trace(trace)
fig.update_layout(
    template="plotly_dark",
    xaxis_title="Lagged Return",
    yaxis_title="Return",
    yaxis_title_font_size=24,
    xaxis_title_font_size=24,
    font_size=20,
    xaxis_tickformat=".0%",
    yaxis_tickformat=".0%",
    width=1000,
    height=500,
)
fig.show()

No, the autocorrelation is almost zero.

Autocorrelations of absolute returns

Absolute returns are a measure of volatility. . . .

Code
plt.style.use('classic')
plt.rcParams.update({'font.size': 26})
fig = plot_acf(spy.ret.abs())
fig.set_size_inches(18,9)
plt.xlabel("Year")
plt.ylabel("Autocorrelation")
_ = plt.title("")

Does today’s absolute return predict tomorrow’s?

Code
import plotly.io as pio
plotly_template = pio.templates["plotly_dark"]
colors = plotly_template.layout.colorway
blue = colors[0]
spy['absret'] = spy.ret.abs()
spy['abslag'] = spy.lag.abs()
trace = go.Scatter(
  x=spy.abslag,
  y=spy.absret,
  text=spy.date,
  mode="markers",
  hovertemplate="%{text}<extra></extra>",
  showlegend=False,
  marker=dict(size=10)
)
fig = go.Figure(trace)
result = smf.ols("absret ~ abslag", data=spy).fit()
spy['predict'] = result.params['Intercept'] + result.params['abslag']*spy.abslag
spy = spy.sort_values(by="abslag")
trace = go.Scatter(
  x=spy.abslag,
  y=spy.predict,
  mode="lines",
  name="regression line",
)
fig.add_trace(trace)
fig.update_layout(
    template="plotly_dark",
    xaxis_title="Lagged Absolute Return",
    yaxis_title="Absolute Return",
    yaxis_title_font_size=24,
    xaxis_title_font_size=24,
    font_size=20,
    xaxis_tickformat=".0%",
    yaxis_tickformat=".0%",
    xaxis_rangemode="tozero",
    yaxis_rangemode="tozero",
    width=1000,
    height=500,
)
fig.show()

Yes, volatility is persistent.

Long-Run Risks

Betting on the stock market

  • Based on history, the bet is definitely in our favor.

  • Play for a long time \(\Rightarrow\) almost certainly come out ahead.

  • But how far ahead is quite uncertain.

    • In worst 20-year period in U.S. stock market since 1926, $1 \(\rightarrow\) $1.73, a geometric average return of 2.8% per year (1929-1948).
    • In best 20-year period since 1926, $1 \(\rightarrow\) $24.65, a geometric average return of 17.4% per year (1980-1999).

Simulate returns

  • Mean and std dev of U.S. market return 1970-2021 was 12.5% and 17.4%.
  • Simulate 20-year compounded returns.
import numpy as np
mn = 0.125
sd = 0.174
nyears = 20
r = np.random.normal(loc=mn, scale=sd, size=nyears)
comp_ret = np.prod(1+r)

Repeat and evaluate the distribution

nsims = 1000
r = np.random.normal(loc=mn, scale=sd, size=nyears*nsims)
r = r.reshape((nyears, nsims))
comp_ret = np.prod(1+r, axis=0)
Code
import numpy as np
import plotly.graph_objects as go
mn = 0.125
sd = 0.174
nsims = 1000
r = np.random.normal(loc=mn, scale=sd, size=30*nsims)
r = r.reshape((30,1000))
comp_ret = np.prod(1+r, axis=0)
trace = go.Box(
  y=comp_ret,
  name="",
  hovertemplate="%{text}<extra></extra>",
)
fig = go.Figure(trace)
fig.update_layout(
    template="plotly_dark",
    yaxis_title="Accumulation from $1",
    xaxis_title="",
    yaxis_title_font_size=20,
    xaxis_title_font_size=20,
    font_size=16,
    yaxis_tickprefix="$",
    yaxis_tickformat=".0f",
    xaxis_tickformat="",
    height=360,
    width=500
)
fig.show()
Code
import pandas as pd
comp_ret = pd.Series(comp_ret)
table = comp_ret.describe(percentiles=[0.1,0.25,0.5,0.75,0.9])
print(table.iloc[1:].round(2))
mean     34.09
std      31.03
min       1.11
10%       8.55
25%      13.73
50%      23.93
75%      43.42
90%      72.90
max     315.03
dtype: float64

Retirement Planning Simulation

Uncertainty about long-run returns \(\Rightarrow\) uncertainty about retirement plans.

  • Revisit the retirement plan
  • Generate random returns and simulate many lifetimes.